Conversation
184531e to
eeaf668
Compare
* Stop adding to-one-joined entity keys to query identifiers * Prune unneeded to-one JOINs Closes dotnet#29182
eeaf668 to
4a3ef03
Compare
There was a problem hiding this comment.
Pull request overview
This PR improves query SQL generation by avoiding redundant identifier expansion and enabling pruning of unnecessary to-one reference joins, especially benefiting split queries (issue #29182).
Changes:
- Treat single-result/to-one joins as not increasing cardinality, so inner identifiers aren’t added to the outer query identifier.
- Detect to-one joins via join predicates and mark corresponding LEFT JOINs as prunable.
- Update many SQL assertion baselines across SqlServer/Sqlite tests to reflect fewer JOINs, projections, and ORDER BY columns.
Reviewed changes
Copilot reviewed 60 out of 60 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs | Adds to-one join awareness to identifier propagation and marks certain LEFT JOINs as prunable; introduces predicate-based to-one detection. |
| test/EFCore.Sqlite.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesSqliteTest.cs | Updates SQL baselines to remove now-pruned LEFT JOIN subqueries/parameters. |
| test/EFCore.Sqlite.FunctionalTests/BulkUpdates/NonSharedModelBulkUpdatesSqliteTest.cs | Updates SQL baseline to remove redundant LEFT JOIN. |
| test/EFCore.SqlServer.FunctionalTests/BulkUpdates/NorthwindBulkUpdatesSqlServerTest.cs | Updates SQL baselines to remove now-pruned LEFT JOIN subqueries/parameters. |
| test/EFCore.SqlServer.FunctionalTests/BulkUpdates/NonSharedModelBulkUpdatesSqlServerTest.cs | Updates SQL baseline to remove redundant LEFT JOIN. |
| test/EFCore.SqlServer.FunctionalTests/Query/*.cs (multiple) | Updates many SQL baselines (fewer projected key columns, fewer JOINs, simplified ORDER BY). |
| Span<bool> matched = innerIdentifiers.Count <= 8 | ||
| ? stackalloc bool[innerIdentifiers.Count] | ||
| : new bool[innerIdentifiers.Count]; | ||
|
|
There was a problem hiding this comment.
AllInnerIdentifiersInPredicate uses stackalloc bool[...] for the matched buffer, but stackalloc memory isn't guaranteed to be zero-initialized. That can cause random true values, leading to false positives (incorrectly treating a join as to-one and skipping identifiers / pruning joins). Initialize the span (e.g., matched.Clear() right after allocation) or avoid stackalloc here.
| matched.Clear(); |
| if (_identifier.Count > 0 && innerSelect._identifier.Count > 0) | ||
| { | ||
| switch (joinType) | ||
| if (!isToOneJoin) | ||
| { | ||
| case JoinType.LeftJoin or JoinType.OuterApply: | ||
| _identifier.AddRange(innerSelect._identifier.Select(e => (e.Column.MakeNullable(), e.Comparer))); | ||
| break; | ||
| switch (joinType) | ||
| { | ||
| case JoinType.LeftJoin or JoinType.OuterApply: | ||
| _identifier.AddRange(innerSelect._identifier.Select(e => (e.Column.MakeNullable(), e.Comparer))); | ||
| break; | ||
|
|
||
| case JoinType.RightJoin: | ||
| var nullableOuterIdentifier = _identifier.Select(e => (e.Column.MakeNullable(), e.Comparer)).ToList(); | ||
| _identifier.Clear(); | ||
| _identifier.AddRange(nullableOuterIdentifier); | ||
| _identifier.AddRange(innerSelect._identifier); | ||
| break; | ||
| case JoinType.RightJoin: | ||
| var nullableOuterIdentifier = _identifier.Select(e => (e.Column.MakeNullable(), e.Comparer)).ToList(); | ||
| _identifier.Clear(); | ||
| _identifier.AddRange(nullableOuterIdentifier); | ||
| _identifier.AddRange(innerSelect._identifier); | ||
| break; | ||
|
|
||
| default: | ||
| _identifier.AddRange(innerSelect._identifier); | ||
| break; | ||
| default: | ||
| _identifier.AddRange(innerSelect._identifier); | ||
| break; |
There was a problem hiding this comment.
The isToOneJoin optimization currently skips all identifier merging, including the special JoinType.RightJoin handling. For a right join, leaving _identifier as the outer identifiers can be incorrect since the preserved side is the inner; this may break row-identity/order-by requirements. Consider excluding RightJoin from the isToOneJoin shortcut, or explicitly setting identifiers for RightJoin (e.g., ensure inner identifiers remain the identifier set when the join is to-one).
Closes #29182